package cn.tedu.charging.cost.service.impl;

import cn.tedu.charging.common.pojo.param.ChargingProcessParam;
import cn.tedu.charging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.dao.repository.ChargingDataESRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleRepository;
import cn.tedu.charging.cost.pojo.ChargingData;
import cn.tedu.charging.cost.pojo.po.ChargingDataPO;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    /**
     * 全局变量 用来存储 充电进度数据
     * Key 订单号 value 充电数据  订单号 度数 花费 次数
     *
     * >1 数据丢失问题
     *   在内存里，如果服务重启 数据丢失 用来累加的数据也丢失了
     *
     * >2 分布式数据共享问题
     * 计价服务 部署了 3台
     * 4040 本地的 Map
     * 4041 本地的 Map
     * 4042 本地的 Map
     * 设备同步数据到订单服务 订单服务 访问通过 负载均衡 访问 计价服务
     * 第一次同步 4040  真实的第一次
     * 第二次同步 4041  对于4041来说 是第一次  对整个订单不是第一次 虚假的第一次
     *
     *>3 数据量问题
     * 价服务 部署到了线上的服务器
     * 一直运行，不重启，运行了一个月  100000个订单，每个订单平均充满需要2个小时
     * 数据每30s 同步一次  数据量很大 容易导致内存溢出 OOM
     *
     * >4 线程安全问题
     * HashMap<> 不是线程安全的Map 应该使用线程安全的 ConcurrentHashMap
     *
     * 怎么解决 上面的 4个问题 ？
     * 存储到其他的介质 比如 数据库
     *
     * 计价服务的数据特点 对数据库的要求 用哪个数据库？
     *
     * 计价服务要存储大量的数据，数据量很大，为什么量大，是因为大量的设备在同时同步数据
     *
     * 数据库 符合
     * 1 海量数据 能存储大量数据
     * 2 高性能 能很快存和取
     * 3 高可用 一直能 高性能 的存和取
     * https://blog.csdn.net/qq_33034733/article/details/140869494  软件系统架构设计的“三高”
     *
     *
     *
     * MySql 关系型数据库
     * 不是海量数据 数据量少 单机Mysql的表数据存储的大小是有规定 如果太大性能就差
     * 不是高性能 严格的ACID 性能相对较差 基于磁盘存储的
     * 是高可用 Mysql集群架构
     *
     * Redis 非关系型数据库
     * 不是海量数据 内存贵 内存的容量有限 持久化 会导致性能降低
     * 高性能 基于内存 内存数据库
     * 是高可用 集群架构 哨兵 Cluster
     *
     * ES
     * 高性能
     * 海量数据
     * 高可用
     *
     *  * 关系型   Mysql Sqlserver Oracle
     *  * 非关系  Redis  K V
     *  * 非关系  MongoDB doc文档
     *  * 非关系  ES   doc文档
     *
     */
    private Map<String,ChargingData> chargingDataMap = new HashMap<>();

    @Override
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {
        // 1. 获取当前时间 小时
        Integer currentHour = getCurrentHour();
        log.debug("获取当前时间小时:{}",currentHour);
        // 2. 获取匹配的计费规则
        Integer stationId = chargingProcessParam.getStationId();
        Integer gunType = chargingProcessParam.getGunType();
        CostRulePO costRulePO = getMatchCostRuleByGunTypeAndTime(stationId,
                gunType,currentHour);
        log.debug("通过枪类型:{}," + "时间:{}," +
                "场站id:{}" + "获取匹配的计费规则:{}",
                gunType, currentHour,
                stationId, costRulePO);
        if (costRulePO != null) {
            // 获取计价规则名称
            String name = costRulePO.getName();
            // 获取计价规则每度电的价格
            BigDecimal powerFee = costRulePO.getPowerFee();
            // 获取设备同步的充电度数
            Float chargingCapacity = chargingProcessParam.getChargingCapacity();


            //ChargingData chargingData = getChargingDataByOrderNo(chargingProcessParam.getOrderNo(),
            //        chargingProcessParam.getChargingCapacity(), new BigDecimal(1));

            //把从本地map中获取充电数据修改为 从ES 获取充电数据
            ChargingDataPO chargingData =
                    getChargingDataByOrderNoFromES(chargingProcessParam.getOrderNo(),
                            chargingProcessParam.getUserId());

            log.debug("通过订单号:{} 获取充电数据:{}",chargingProcessParam.getOrderNo(),chargingData);
            //代码重构，理解把 变化的 不变 的进行分离
            //first 和 last 两个方法的基本逻辑是一样的
            //区别是 充电度数计算 和 总花费的计算 不同
            //把不同的逻辑 抽取到 chargingData 让 第一次计算和第N次计算 复用
            cost2ChargingData(chargingData,chargingCapacity,powerFee,currentHour,name);

            return buildChargingProcessVO(chargingData);

            //判断是否是第一次同步数据
           // if (chargingData.isFirst()) {
                //第一次同步数据
           //     firstCost2ChargingData(chargingData,chargingCapacity,powerFee,currentHour,name);
           // }else {
                //第N次同步数据
            //    lastCost2ChargingData(chargingData,chargingCapacity,powerFee,currentHour,name);
           // }
        }else {
            log.error("故障，计价规则为空,进行系统告警，业务停止");
        }
        return null;
    }

    /**
     * 通过充电数据 构建 计价服务计算 价格的出参
     * @param chargingData
     * @return
     */
    private ChargingProcessVO buildChargingProcessVO(ChargingDataPO chargingData) {
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();
        chargingProcessVO.setOrderNo(chargingData.getOrderNo());
        chargingProcessVO.setTotal(chargingData.getTotalFee());
        chargingProcessVO.setChargingCapacity(chargingData.getChargingCapacity());
        //如果参数过多，可以通过BeanUtils工具类进行拷贝  copy
        //为啥能 copy 属性值
        //BeanUtils.copyProperties 原理
        //1 获取源 source chargingData.class 获取getter readMethod 读方法
        //2 获取目标 target ChargingProcessVO.class 获取setter writeMethod 写方法
        //3 循环遍历 readMethod 读方法
        //4 判断 读方法 和 写方法 的访问修饰符 如果不是 public setAccessible(true) 强制访问
        //5 通过 source的读方法 读取source的值 Object value = readMethod.invoke(source);
        //6 通过 writeMethod 写方法 把值写入到target中 writeMethod.invoke(target, value);
        //属性名 必须一致才能拷贝 属性类型 会自动转换 能转换成功
        BeanUtils.copyProperties(chargingData,chargingProcessVO);
        //充电总时长的计算  充电的结束时间 - 充电的开始时间
        //结束时间 在充满后 设备告诉订单 充满的时间 是结束时间
        //开始时间  设备返回 开始充电响应结果的时间 存储在订单成功表 ,为了避免订单传输这个时间
        //开始时间  设备第一次同步充电数据的时间
        //开始时间
        Long startTime = chargingData.getStartTime();
        //获取当前时间
        long now = System.currentTimeMillis();
        // 获取当前时间 - 开始时间 = 已经充电的时间
        //LocalDateTime now1 = LocalDateTime.now();
        //Duration.between(now1,now)
        //消耗的时间
        return chargingProcessVO;
    }


    /**
     * 2 two to
     * 计算第一次充电的价格 和度数  保存到充电数据
     * @param chargingData  充电数据
     * @param chargingCapacity 度数
     * @param powerFee 计价规则的电费单价
     */
    private void cost2ChargingData(ChargingDataPO chargingData,
                                        Float chargingCapacity,BigDecimal powerFee,
                                        Integer currentHour,String name) {
        log.debug("第一次同步数据，计算价格");
        Float realChargingCapacity = chargingData.getChargingCapacity(chargingCapacity);
        // 计算价格 充电度数 * 每度电的价格
        BigDecimal cost = getCost(realChargingCapacity, powerFee);
        //保存 度数  到 chargingData 为了下一次<<<计算实际充电度数>>>做准备
        chargingData.setChargingCapacity(chargingCapacity);
        //保存 当前花费 到  chargingData 为了下一次 <<<计算真实总花费>>> 做准备
        chargingData.setTotalFee(cost);
        log.debug("当前时间:{}，计价规则的名称:{}," +
                        "充电度数:{},电费单价:{}," +
                        "本次花费:{},总花费:{}",
                currentHour,name,
                realChargingCapacity,powerFee,
                cost,chargingData.getTotalFee());
        //保存修改后的充电数据到ES
        log.debug("第{}次同步数据，保存充电数据到ES:{}",chargingData.getCount(),chargingData);
        chargingDataESRepository.save(chargingData);
        log.debug("第{}次同步数据，保存充电数据到ES成功:{}",chargingData.getCount(),chargingData);
    }

    /**
     * 2 two to
     * 计算第一次充电的价格 和度数  保存到充电数据
     * @param chargingData  充电数据
     * @param chargingCapacity 度数
     * @param powerFee 计价规则的电费单价
     */
    private void firstCost2ChargingData(ChargingDataPO chargingData,
                                        Float chargingCapacity,BigDecimal powerFee,
                                        Integer currentHour,String name) {
        log.debug("第一次同步数据，计算价格");
        // 计算价格 充电度数 * 每度电的价格
        BigDecimal cost = getCost(chargingCapacity, powerFee);
        //保存 度数  到 chargingData 为了下一次<<<计算实际充电度数>>>做准备
        chargingData.setChargingCapacity(chargingCapacity);
        //保存 当前花费 到  chargingData 为了下一次 <<<计算真实总花费>>> 做准备
        chargingData.setTotalFee(cost);
        log.debug("当前时间:{}，计价规则的名称:{}," +
                        "充电度数:{},电费单价:{}," +
                        "本次花费:{},总花费:{}",
                currentHour,name,
                chargingCapacity,powerFee,
                cost,cost);
        //保存修改后的充电数据到ES
        log.debug("第{}次同步数据，保存充电数据到ES:{}",chargingData.getCount(),chargingData);
        chargingDataESRepository.save(chargingData);
        log.debug("第{}次同步数据，保存充电数据到ES成功:{}",chargingData.getCount(),chargingData);
    }

    /**
     * 2 two to
     * 计算第N次充电的价格和度数到充电数据
     * @param chargingData  充电数据
     * @param chargingCapacity 度数
     * @param powerFee 计价规则的电费单价
     */
    private void lastCost2ChargingData(ChargingDataPO chargingData,
                                       Float chargingCapacity,BigDecimal powerFee,
                                       Integer currentHour,String name) {
        log.debug("第N次同步数据，计算价格");
        // 计算价格 充电度数 * 每度电的价格
        // 实际充电度数 = 当前同步充电度数 - 上次同步充电度数
        Float realChargingCapacity = chargingData.getChargingCapacity(chargingCapacity);
        BigDecimal cost = getCost(realChargingCapacity, powerFee);
        //保存 度数  到 chargingData 为了下一次<<<计算实际充电度数>>>做准备
        chargingData.setChargingCapacity(chargingCapacity);
        //获取上次的花费
        //BigDecimal lastFee = chargingData.getTotalFee();
        //当前总花费 = 上次花费 + 本次花费
        //BigDecimal totalFee = lastFee.add(cost);
        //保存 当前总花费 保存到  chargingData 为了下一次 <<<计算真实总花费>>> 做准备
        chargingData.setTotalFee(cost);
        log.debug("当前时间:{}，计价规则的名称:{}," +
                        "充电度数:{},电费单价:{}," +
                        "本次花费:{},总花费:{}",
                currentHour,name,
                chargingCapacity,powerFee,
                cost,chargingData.getTotalFee());

        //保存修改后的充电数据到ES
        log.debug("第{}次同步数据，保存充电数据到ES:{}",chargingData.getCount(),chargingData);
        chargingDataESRepository.save(chargingData);
        log.debug("第{}次同步数据，保存充电数据到ES成功:{}",chargingData.getCount(),chargingData);
    }

    @Autowired
    private ChargingDataESRepository chargingDataESRepository;

    /**
     * 通过订单号 从ES 查询 充电数据
     * 解决本地map的问题
     * @param orderNo
     * @return
     */
    private ChargingDataPO getChargingDataByOrderNoFromES(String orderNo,Integer userId) {
        Optional<ChargingDataPO> optional = chargingDataESRepository.findById(orderNo);
        // optional.isPresent() 判断 optional 是否有数据  value != null;
        if (optional.isPresent()) {
            //chargingData 不等于null 表示 不是第一次 第 N次 同步数据
            //获取上次的次数
            ChargingDataPO chargingDataPO = optional.get();
            Integer count = chargingDataPO.getCount();
            //上次的次数 + 本次 = 总次数
            count = count + 1;
            //总次数 保存到 充电数据
            chargingDataPO.setCount(count);
            log.debug("第{}次同步数据，保存充电数据到ES:{}",count,chargingDataPO);
            chargingDataESRepository.save(chargingDataPO);
            log.debug("第{}次同步数据，保存充电数据到ES成功:{}",count,chargingDataPO);
            return chargingDataPO;
        }else {

            //chargingData 等于null 表示 第一次同步数据
            //创建 同步数据 对象
            ChargingDataPO chargingData = new ChargingDataPO();
            chargingData.setId(orderNo);
            chargingData.setOrderNo(orderNo);
            chargingData.setUserId(userId);
            chargingData.setStartTime(System.currentTimeMillis());
            Integer count = 1;
            chargingData.setCount(count);
            //保存 同步数据 到map 为了下一次同步做准备
            log.debug("第{}次同步数据，保存充电数据到ES:{}",count,chargingData);
            chargingDataESRepository.save(chargingData);
            log.debug("第{}次同步数据，保存充电数据到ES成功:{}",count,chargingData);
            return chargingData;
        }
    }

    /**
     * 通过订单号 获取 充电数据
     * @param orderNo
     * @return
     */
    private ChargingData getChargingDataByOrderNo(String orderNo,
                                                  Float chargingCapacity,
                                                  BigDecimal totalFee) {
        //Map<String,ChargingData> chargingDataMap = new HashMap<>();
        ChargingData chargingData = chargingDataMap.get(orderNo);
        if (chargingData == null) {
            //chargingData 等于null 表示 第一次同步数据
            //创建 同步数据 对象
            chargingData = new ChargingData();
            chargingData.setOrderNo(orderNo);
            //chargingData.setChargingCapacity(chargingCapacity);
            //chargingData.setTotalFee(totalFee);
            chargingData.setCount(1);
            //保存 同步数据 到map 为了下一次同步做准备
            chargingDataMap.put(orderNo,chargingData);
        }else {
            //chargingData 不等于null 表示 不是第一次 第N次 同步数据
            //获取上次的次数
            Integer count = chargingData.getCount();
            //上次的次数 + 本次 = 总次数
            count = count + 1;
            //总次数 保存到 充电数据
            chargingData.setCount(count);

            //获取上次的花费
            //BigDecimal lastTotalFee = chargingData.getTotalFee();
            //上次的花费 + 本次的花费 = 总的花费
            //lastTotalFee =  lastTotalFee.add(totalFee);
            //总花费 保存到 充电数据
            //chargingData.setTotalFee(lastTotalFee);


            //获取上次的充电度数
            //Float lastChargingCapacity = chargingData.getChargingCapacity();
            //实际充电度数 = 本次的充电度数 - 上次的充电度数
            //Float realChargingCapacity = chargingCapacity - lastChargingCapacity;
            //log.debug("本次同步的实际充电度数:{}",realChargingCapacity);

            //保存本次的充电度数
            //chargingData.setChargingCapacity(chargingCapacity);

            //保存 同步数据 到map 为了下一次同步做准备
            //chargingDataMap.put(orderNo,chargingData);
        }
        return chargingData;
    }

    /**
     * 计算价格 充电度数 * 每度电的价格
     * @param chargingCapacity 充电度数
     * @param powerFee 每度电的价格
     * @return
     */
    private BigDecimal getCost(Float chargingCapacity, BigDecimal powerFee){
        //1>通过构造器 进行类型转换
        //Float 转换为 BigDecimal
        //BigDecimal bigDecimal = new BigDecimal(chargingCapacity);
        //基本类型的数字转换为 包装类型的 Integer
        //Integer i = new Integer(1);
        //String 转换为 包装类型的 String
        //String s = new String("1");

        //2>通过方法 进行类型转换 valueOf
        //通过方法 valueOf 转换 float 转为 String
        //String s = String.valueOf(chargingCapacity);
        //Float 转换为 BigDecimal
        BigDecimal bigDecimal = BigDecimal.valueOf(chargingCapacity);
        //BigDecimal add 加计算  multiply 乘法
        return powerFee.multiply(bigDecimal);
    }

    /**
     * 获取当前小时
     * @return
     */
    private Integer getCurrentHour(){
        //@Deprecated
        //Date date = new Date();
        //int date1 = date.getDate();
        //joda-time
        //LocalDateTime 比 Date 更新 更好用
        LocalDateTime now = LocalDateTime.now();
        return now.getHour();
    }

    /**
     * 通过  时间 和 枪类型 匹配一个计价规则
     * @param stationId
     * @param gunType
     * @param hour
     * @return
     */
    private CostRulePO getMatchCostRuleByGunTypeAndTime(Integer stationId,
                                                        Integer gunType,
                                                        Integer hour){
        log.debug("通过场站id:{}获取计价规则列表",stationId);
        //1>从数据库查询当前站id的所有计价规则列表，通过程序匹配 lambda 或者 for循环
        long start = System.currentTimeMillis();
        List<CostRulePO> costRules = getCostRules(stationId);
        long end = System.currentTimeMillis();
        long costTime = end - start;
        //2> 通过sql语句去匹配计价规则
        //1 和 2 都能实现匹配计价规则
        //因为设备会高频的同步充电进度数据，会导致高频的计算价格，会导致高频的匹配计价规则
        // 会导致高频的查询数据库 1 和 2 都会查询数据库，todo 通过加缓存的方式，减少数据库的查询
        //CostRulePO costRule = costRuleRepository.getMatchCostRule(stationId,gunType,hour);
        log.debug("通过场站id:{}获取计价规则列表:{},消耗时间:{}",stationId,costRules,costTime);
        //判断计价规则列表是否为空
        if (!CollectionUtils.isEmpty(costRules)) {


        /*    Optional<CostRulePO> first = costRules.stream().filter(e -> e.getGunType().equals(gunType)
                    && hour >= e.getStartTime()
                    && hour < e.getEndTime()).findFirst();
            //  first.isPresent() 就是 return value != null;
            if (first.isPresent()) {
                return first.get();
            }*/



            for (CostRulePO costRule : costRules) {
                //获取计价规则的枪类型
                Integer costRuleGunType = costRule.getGunType();
                //判断枪类型是否匹配
                if (costRuleGunType.equals(gunType)) {
                    //获取计价规则的开始时间
                    Integer startTime = costRule.getStartTime();
                    //获取计价规则的结束时间
                    Integer endTime = costRule.getEndTime();
                    /**
                     *  当前时间 10:45  获取到小时数 10
                     *  找到真实的计价规则 通过人肉的方式 去判断  当前时间属于哪个时间段
                     *   8,12
                     *   14,18
                     *   18,24
                     *   12,14
                     *   0,8
                     *  真实的计价规则
                     *
                     *  10 在 8 和 12 之间
                     *
                     *  10 > 8 并且 10 < 12
                     *  hour > startTime && hour < endTime
                     *
                     *  边界值的处理
                     *  如果当前时间是 8 如何处理？
                     *  如果当前时间是 12 如何处理？
                     *  如果当前时间是 8 是 大于 还是 大于等于？
                     *  如果当前时间是 12 是 大于 还是 大于等于？
                     *
                     */
                    if (hour >= startTime && hour < endTime) {
                        return costRule;
                    }
                }
            }
        }
        //循环完了，都没有匹配到，表示没有匹配到计价规则
        return null;
    }

    @Autowired
    private CostRuleRepository costRuleRepository;

    @Autowired
    private CostRuleCacheRepository costRuleCacheRepository;

    /**
     * 通过场站id获取计价规则列表
     * getCostRules 目前是通过数据库查询
     * 目标 是 从缓存查询 提高性能
     * 从缓存查询 的前提 是 缓存中有
     * 缓存中什么时候有呢？
     *
     * 1 <<旁路缓存>> cacheAside  能用缓存预热的不一定能用旁路缓存
     *    1.1 先从Redis查询
     *    1.2 判断Redis是否有数据
     *    1.3 有 返回，直接返回
     *    1.4 没有，从数据库查询，然后保存到Redis，返回
     *
     *
     * 2 <<缓存预热>> 能用旁路缓存的一定能用缓存预热
     *  项目启动的时候
     *  从数据库把计价规则查询出来，然后保存到Redis,
     *  查询的时候直接从Redis查，不从数据库查
     *
     *
     * @param stationId
     * @return
     */
    private List<CostRulePO> getCostRules(Integer stationId) {
        log.debug("通过场站id:{},从Redis查询计价规则",stationId);
        List<CostRulePO> cacheCostRules =
                costRuleCacheRepository.getCostRuleByStationId(stationId);
        log.debug("通过场站id:{},从Redis查询计价规则列表:{}",stationId,cacheCostRules);
        //判断Redis查询出的计价规则是否为空
        if(!CollectionUtils.isEmpty(cacheCostRules)) {
            log.debug("通过场站id:{},从Redis查询计价规则列表，<<Redis有数据>>，直接返回",
                    stationId,cacheCostRules);
            return cacheCostRules;
        }else {
            log.debug("通过场站id:{},从Redis查询计价规则列表，<<Redis没有数据>>，从数据库查询",
                    stationId);
            List<CostRulePO> dbCostRules =
                    costRuleRepository.getCostRulesByStationId(stationId);
            log.debug("通过场站id:{},从Redis查询计价规则列表，Redis没有数据，从数据库查询计价规则列表:{}",
                    stationId,dbCostRules);
            if (!CollectionUtils.isEmpty(dbCostRules)) {
                log.debug("通过场站id:{},从数据库查询计价规则列表:{}，数据库有数据，写入Redis,并返回",
                        stationId,dbCostRules);
                costRuleCacheRepository.saveCostRules(stationId,dbCostRules);
                return dbCostRules;
            }
        }
        return Collections.emptyList();
    }

}
